<template>
  <div class="ek-captchas-slider">
    <!-- 画布区域 -->
    <div class="captchas-box">
      <!-- 主要渲染整体背景及镂空图案 -->
      <canvas class="canvas-bg" :width="width" :height="height"></canvas>
      <canvas
        class="canvas-block"
        :height="height"
        :style="{ left: `${blockLeft}px` }"
      ></canvas>
    </div>
    <!-- 底部滑块 -->
    <div
      :class="['slider-wrap', status]"
      :style="{ width: `${width}px` }"
      @mouseleave="handleDrapUp"
    >
      <div
        class="progress"
        :style="{
          width: `${blockLeft}px`,
        }"
      ></div>
      <div
        class="slider-block"
        ref="sliderRef"
        :style="{ left: `${blockLeft}px` }"
        @mousedown="handleDrapDown"
        @mouseup="handleDrapUp"
        @mousemove="handleDragMove"
        @touchstart="handleDrapDown"
        @touchmove="handleDragMove"
        @touchend="handleDrapUp"
      >
        &rarr;
      </div>
    </div>
    <!-- 刷新按钮 -->
    <div class="refresh" @click="onRefreshBtnClick">
      <i class="icon iconfont icon-refresh"></i>
    </div>
    <!-- 加载中提示 -->
    <div
      class="loading"
      v-show="isLoading"
      v-loading="isLoading"
      :style="{ width: `${width}px`, height: `${height}px` }"
    ></div>
  </div>
</template>
<script setup>
// https://juejin.cn/post/7017640232765898789  参考地址
import {
  defineComponent,
  onMounted,
  watch,
  ref,
  nextTick,
  defineEmits,
} from "vue";
import img1 from "@assets/image/verify/1.jpg";
const props = defineProps({
  // 组件的宽度
  width: {
    type: Number,
    default: 300,
  },
  // 组件的高度
  height: {
    type: Number,
    default: 200,
  },
  // 滑块的长度
  blockLength: {
    type: Number,
    default: 42,
  },
  // 滑块的半径
  blockRadius: {
    type: Number,
    default: 9,
  },
  // 滑块的容错值
  tolerant: {
    type: Number,
    default: 5,
  },
  targetConfigX: {
    type: Number,
    default: 0,
  },
});
const emit = defineEmits(["success", "fail", "refresh"]);
// 是否出现loading
const isLoading = ref(true);
//是否可拖拽
const isDrop = ref(false);
// 滑块的位置
const blockLeft = ref(0);
// 滑块的状态
const status = ref("");
// 滑块移动的目标位置
const tagetX = ref(0);
const blockInitLeft = ref(0);
const sliderRef = ref(null);

onMounted(() => {
  initCanvas();
  nextTick(() => {
    blockInitLeft.value = sliderRef.value.getBoundingClientRect().left;
  });
});
// 初始化canvas画布，主要功能：
/**
 * 1. 随机获取到图片
 * 2. 随机生成图片滑块位置
 * 3. 截取图片并移动到初始位置
 * 4. 画出镂空图案
 * 5. 拿到下方滑块的初始位置
 */
const initCanvas = () => {
  // 拿到canvas对象
  let canvas = document.getElementsByClassName("canvas-bg")[0];
  let blockcanvas = document.getElementsByClassName("canvas-block")[0];
  // 画笔
  let ctx = canvas.getContext("2d");
  let blockCtx = blockcanvas.getContext("2d");
  // 每次重新绘制图片时，清除画布原有内容
  ctx.clearRect(0, 0, props.width, props.height);
  blockcanvas.width = props.width;
  // 生成image对象
  let img = new Image();
  // 解决跨域问题
  img.crossOrigin = "";
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    blockCtx.drawImage(img, 0, 0);
    isLoading.value = false;
    const ImageData = blockCtx.getImageData(
      x,
      y1,
      props.blockLength + 2 * props.blockRadius,
      y
    );
    // 裁剪后重置画布的状态
    blockCtx.restore();
    blockcanvas.width = props.blockLength + 2 * props.blockRadius;
    blockCtx.putImageData(ImageData, 0, y1);
  };
  // 图片访问地址,后面增加时间戳,防止拿缓存
  img.src = img1;

  // x y 位置随机
  let { x, y } = getXY();
  drawPath(x, y, props.blockLength, props.blockRadius, ctx, "fill");
  // 裁剪前保存画布的状态
  blockCtx.save();
  drawPath(x, y, props.blockLength, props.blockRadius, blockCtx, "clip");
  const y1 = y - props.blockRadius * 2 - 1;
};

// 重置按钮
const onRefreshBtnClick = () => {
  handleRefresh();
};
const handleRefresh = () => {
  isLoading.value = true;
  emit("refresh", () => {
    blockLeft.value = 0;
    status.value = "";
    initCanvas();
  });
};

// 画出遮罩&滑块
const drawPath = (x, y, l, r, ctx, operation) => {
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.arc(x + l / 2, y - r + 2, r, 0.72 * Math.PI, 2.26 * Math.PI);
  ctx.lineTo(x + l, y);
  ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * Math.PI, 2.78 * Math.PI);
  ctx.lineTo(x + l, y + l);
  ctx.lineTo(x, y + l);
  // anticlockwise为一个布尔值。为true时，是逆时针方向，否则顺时针方向
  ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * Math.PI, 1.24 * Math.PI, true);
  ctx.lineTo(x, y);
  ctx.lineWidth = 2;
  ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
  ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-over";
  // 判断是填充还是裁切, 裁切主要用于生成图案滑块
  operation === "fill" ? ctx.fill() : ctx.clip();
};
// 随机生成滑块的目标位置
const getXY = () => {
  let x =
    props.width / 3 + Math.random() * ((props.width * 3) / 4 - props.width / 3);
  let y =
    props.height / 3 +
    Math.random() * ((props.height * 3) / 5 - props.height / 3);
  console.log(x);
  // 存在配置的位置时, 使用配置的位置
  if (props.targetConfigX) {
    x = props.targetConfigX;
  }
  tagetX.value = x;
  return { x, y };
};

const handleDragMove = (e) => {
  if (!isDrop.value) return false;
  e.preventDefault();
  const eventX = e.clientX || e.touches[0].clientX;
  let moveX = eventX - blockInitLeft.value - 20;
  if (moveX < 0 || moveX + 40 + 2 * props.blockRadius > props.width)
    return false;
  // 设置滑块的的位置及进度条的宽度
  blockLeft.value = moveX;
};

// 初始化未完成时,不可滑动
// 滑动开始时,将状态置为等待状态
const handleDrapDown = (e) => {
  if (isLoading.value) return;
  isDrop.value = true;
  status.value = "wait";
};
const handleDrapUp = (e) => {
  if (!isDrop.value) return;
  isDrop.value = false;
  let val = Math.abs(tagetX.value - blockLeft.value);
  status.value = val > props.tolerant ? "fail" : "success";
  if (status.value === "fail") {
    emit("fail");
    handleRefresh();
  } else {
    emit("success", blockLeft.value);
  }
};
</script>

<style lang="scss" scoped>
.ek-captchas-slider {
  position: relative;

  > .captchas-box {
    position: relative;
    .canvas-bg {
    }
    .canvas-block {
      position: absolute;
    }
  }
}
.slider-wrap {
  height: 40px;
  box-sizing: border-box;
  margin-top: 5px;
  background-color: #efefef;
  border: 1px solid #cbcbcb;
  .slider-block {
    width: 40px;
    height: 100%;
    box-shadow: 0 0 5px rgb(209, 209, 209);
    background-color: #fff;
    font-weight: bold;
    text-align: center;
    line-height: 40px;
    font-size: 20px;
    cursor: pointer;
    user-select: none;
    position: relative;
  }
  .progress {
    position: absolute;
    box-sizing: border-box;
    height: 40px;
  }
}

/* 不同状态显示不同的颜色 */
.wait .progress {
  background-color: #c7defb;
  border: 1px solid #409eff;
}
.wait .slider-block {
  background-color: #409eff;
  border: 1px solid #409eff;
  color: #fff;
}
.fail .progress {
  background-color: #f9d8d6;
  border: 1px solid var(--el-color-danger-light-3);
}
.fail .slider-block {
  border: 1px solid var(--el-color-danger-light-3);
  background-color: var(--el-color-danger-light-3);
  color: #fff;
}
.success .progress {
  background-color: #d2f1eb;
  border: 1px solid var(--el-color-success-light-3);
}
.success .slider-block {
  border: 1px solid var(--el-color-success-light-3);
  background-color: var(--el-color-success-light-3);
  color: #fff;
}
.refresh {
  width: 20px;
  height: 20px;
  position: absolute;
  right: 10px;
  top: 10px;
  cursor: pointer;
  > i {
    font-size: 24px;
    color: #eee;
  }
}
.loading {
  position: absolute;
  box-sizing: border-box;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: #fff;
  text-align: center;
  color: #303030;
  font-size: 14px;
  border: 1px solid rgb(209, 209, 209);
}
.loading img {
  width: 40px;
  height: 40px;
  display: block;
  margin: 30px auto;
  margin-bottom: 15px;
}
</style>
